/* 
 * Copyright (c) 2007 Wayne Meissner
 * 
 * This file is part of gstreamer-java.
 *
 * This code is free software: you can redistribute it and/or modify it under 
 * the terms of the GNU Lesser General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License 
 * version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with this work.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.gstreamer;


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.gstreamer.glib.MainContextExecutorService;
import org.gstreamer.lowlevel.GstAPI;
import org.gstreamer.lowlevel.GMainContext;
import org.gstreamer.lowlevel.GstNative;
import org.gstreamer.lowlevel.GstAPI.GErrorStruct;

import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;

/**
 * Media library supporting arbitrary formats and filter graphs.
 * 
 */
public final class Gst {
    static Logger logger = Logger.getLogger(Gst.class.getName());
    private static ScheduledExecutorService executorService;
    private static volatile CountDownLatch quit = new CountDownLatch(1);
    private static GMainContext mainContext;
    private static boolean useDefaultContext = false;
    private static final AtomicInteger initCount = new AtomicInteger(0);
    private static List<Runnable> shutdownTasks = Collections.synchronizedList(new ArrayList<Runnable>());
    private static final GstAPI gst = GstNative.load(GstAPI.class);    
    
    /** Creates a new instance of Gst */
    private Gst() {
    }
    
    /**
     * Gets the version of gstreamer currently in use.
     * 
     * @return the version of gstreamer
     */
    public static Version getVersion() {
        long[] major = { 0 }, minor = { 0 }, micro = { 0 }, nano = { 0 };
        gst.gst_version(major, minor, micro, nano);
        return new Version(major[0], minor[0], micro[0], nano[0]);
    }
    
    /**
     * Gets the the version of gstreamer currently in use, as a String.
     * 
     * @return a string representation of the version.
     */
    public static String getVersionString() {
        return gst.gst_version_string();
    }
    
    /**
     * Gets the common {@code Executor} used to execute background tasks.
     * 
     * @return an executor that can be used for background tasks.
     */
    public static Executor getExecutor() {
        return getScheduledExecutorService();
    }
    
    /**
     * Gets the common {@code ExecutorService} used to execute background tasks.
     * 
     * @return an executor that can be used for background tasks.
     */
    public static ExecutorService getExecutorService() {
        return getScheduledExecutorService();
    }
    
    /**
     * Gets the common {@code ScheduledExecutorService} used to execute 
     * background tasks and schedule timeouts.
     * 
     * @return an executor that can be used for background tasks.
     */
    public static ScheduledExecutorService getScheduledExecutorService() {
        return executorService;
    }
    
    /**
     * Signals the thread that called {@link #init} to return.
     */
    public static void quit() {
        quit.countDown();
    }
    
    /**
     * Waits for the gstreamer system to shutdown via a call to {@link #quit}.
     * <p> For most gui programs, this is of little use.  However, it can be
     * a convenient way of keeping the main thread alive whilst gstreamer 
     * processing on other threads continues.
     */
    public static void main() {
        try {
            CountDownLatch latch = quit;
            if (latch != null) {
                latch.await();
            }
        } catch (InterruptedException ex) {
        } finally {
            quit = new CountDownLatch(1);
        }
    }
    
    /**
     * Schedules a task for execution on the gstreamer background
     * {@link java.util.concurrent.Executor}.
     *
     * @param task the task to execute.
     */
    public static void invokeLater(final Runnable task) {
        getExecutor().execute(task);
    }
    
    /**
     * Executes a task on the gstreamer background
     * {@link java.util.concurrent.Executor}, waiting until the task completes
     * before returning.
     *
     * @param task the task to execute.
     */
    public static void invokeAndWait(Runnable task) {
        try {
            getExecutorService().submit(task).get();
        } catch (Exception ex) {
            throw new RuntimeException(ex.getCause());
        }
    }
    
    /**
     * Gets the current main context used (if any).
     * 
     * @return a main context.
     */
    public static GMainContext getMainContext() {
        return mainContext;
    }
    
    /**
     * Initializes the GStreamer library.
     * <p> This is a shortcut if no arguments are to be passed to gstreamer.
     *
     * @throws org.gstreamer.GstException
     */
    public static final void init() throws GstException {
        init("unknown", new String[] {});
    }
    
    /**
     * Initializes the GStreamer library.
     * <p> This sets up internal path lists, registers built-in elements, and 
     * loads standard plugins.
     *
     * <p>
     * This method should be called before calling any other GLib functions. If
     * this is not an option, your program must initialise the GLib thread system
     * using g_thread_init() before any other GLib functions are called.
     *
     * <p>
     * <b>Note:</b><p>
     * This method will throw a GstException if it fails.
     * 
     * @param progname the java program name.
     * @param args the java argument list.
     * @return the list of arguments with any gstreamer specific options stripped 
     * out.
     * @throws org.gstreamer.GstException
     */
    public static synchronized final String[] init(String progname, String[] args) throws GstException {
        //
        // Only do real init the first time through
        //
        if (initCount.getAndIncrement() > 0) {
            return args;
        }
        NativeArgs argv = new NativeArgs(progname, args);
        Logger.getLogger("org.gstreamer").setLevel(Level.WARNING);
        
        Pointer[] error = { null };
        if (!gst.gst_init_check(argv.argcRef, argv.argvRef, error)) {
            initCount.decrementAndGet();
            throw new GstException(new GError(new GErrorStruct(error[0])));
        }
        
        logger.fine("after gst_init, argc=" + argv.argcRef.getValue());

        if (useDefaultContext) {
            mainContext = GMainContext.getDefaultContext();
            executorService = new MainContextExecutorService(mainContext);
        } else {
            mainContext = new GMainContext();
            executorService = Executors.newSingleThreadScheduledExecutor(threadFactory);
        }
        quit = new CountDownLatch(1);
        return argv.toStringArray();
    }
    
    /**
     * Undoes all the initialization done in {@link #init}.
     * <p> This will run any cleanup tasks, terminate any timers and other
     * asynchronous tasks, and de-initialize the gstreamer library.
     */
    public static synchronized final void deinit() {
        //
        // Only perform real shutdown if called as many times as Gst.init() is
        //
        if (initCount.decrementAndGet() > 0) {
            return;
        }
        // Perform any cleanup tasks
        for (Object task : shutdownTasks.toArray()) {
            ((Runnable) task).run();
        }
        
        // Stop any more tasks/timers from being scheduled
        executorService.shutdown();
        
        // Wake up the run thread.
        quit(); 
        
        // Wait for tasks to complete.
        try {
            if (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
                // Force-kill everything
                executorService.shutdownNow();
            }
        } catch (InterruptedException ex) { }
        
        mainContext = null;
        System.gc(); // Make sure any dangling objects are unreffed before calling deinit().
        gst.gst_deinit();
    }
    
    /**
     * Adds a task to be called when {@link Gst#deinit} is called.
     * <p> This is used internally, and is not recommended for other uses.
     * 
     * @param task the task to execute.
     */
    public static void addStaticShutdownTask(Runnable task) {
        shutdownTasks.add(task);
    }
    
    /**
     * Instructs gstreamer-java to use the default main context.
     * <p>
     * This may be useful if integration with the GTK main loop is desirable,
     * as all {@link Bus} signals and timers will be executed in the context
     * of the GTK main loop.
     * <p>
     * For the majority of programs though, it is better to wrap the individual
     * listeners in a proxy which executes the listener in the appropriate 
     * context.
     * 
     * @param useDefault if true, use the default glib main context.
     */
    public static void setUseDefaultContext(boolean useDefault) {
        useDefaultContext = useDefault;
    }
    
    // Make the gstreamer executor threads daemon, so they don't stop the main 
    // program from exiting
    private static final ThreadFactory threadFactory = new ThreadFactory() {
        private final AtomicInteger counter = new AtomicInteger(0);
        /**
         * Determines if Gst has been started from an applet and returns 
         * it's parent group.
         * 
         * This is to avoid a problem where the service thread is killed when
         * an applet is destroyed.  If multiple applets are active simultaneously,
         * this could be a problem.
         * 
         * @return Applet's parent ("main") thread group or null, if not 
         * running inside an applet
         */
        private ThreadGroup getThreadGroup() {
            ThreadGroup tg = Thread.currentThread().getThreadGroup();
            try {
                Class<?> atgClass = Class.forName("sun.applet.AppletThreadGroup");
                return atgClass.isInstance(tg) ? tg.getParent() : null;
            } catch (ClassNotFoundException ex) {
                return null;
            }
        }
        public Thread newThread(Runnable task) {
            final String name = "gstreamer service thread " + counter.incrementAndGet();
            Thread t = new Thread(getThreadGroup(), task, name);
            t.setDaemon(true);
            t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    };
}

class NativeArgs {
    IntByReference argcRef;
    PointerByReference argvRef;
    Memory[] argsCopy;
    Memory argvMemory;
    public NativeArgs(String progname, String[] args) {
        //
        // Allocate some native memory to pass the args down to the native layer
        //
        argsCopy = new Memory[args.length + 2];
        argvMemory = new Memory(argsCopy.length * Pointer.SIZE);
        
        //
        // Insert the program name as argv[0]
        //
        Memory arg = new Memory(progname.length() + 4);
        arg.setString(0, progname, false);
        argsCopy[0] = arg;
        
        for (int i = 0; i < args.length; i++) {
            arg = new Memory(args[i].length() + 1);
            arg.setString(0, args[i], false);
            argsCopy[i + 1] = arg;
        }
        argvMemory.write(0, argsCopy, 0, argsCopy.length);
        argvRef = new PointerByReference(argvMemory);
        argcRef = new IntByReference(args.length + 1);
    }
    String[] toStringArray() {
        //
        // Unpack the native arguments back into a String array
        //
        List<String> args = new ArrayList<String>();
        Pointer argv = argvRef.getValue();
        for (int i = 1; i < argcRef.getValue(); i++) {
            Pointer arg = argv.getPointer(i * Pointer.SIZE);
            if (arg != null) {
                args.add(arg.getString(0, false));
            }
        }
        return args.toArray(new String[args.size()]);
    }
    
}